/*
 * $Id: JsonRpcDescription.java 41030 2007-01-18 21:47:55Z jdamick $
 *
 * Copyright 2006- Revolution Health Group.  All rights reserved.
 *
 * This software is the confidential and proprietary information
 * of Revolution Health Group.  (Confidential Information).
 * You shall not disclose such Confidential Information and shall
 * use it only in accordance with the terms of the license 
 * agreement you entered into with Revolution Health Group.
 *
 */
package org.javason.jsonrpc.impl;

import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.beanutils.PropertyUtilsBean;
import org.apache.log4j.Logger;
import net.sf.json.JSONArray;
import net.sf.json.JSONException;
import net.sf.json.JSONObject;

import org.javason.jsonrpc.description.Param;
import org.javason.jsonrpc.description.Proc;
import org.javason.jsonrpc.description.Service;
import org.javason.util.ClassUtils;

public class JsonRpcDescription {
	private static final Logger LOG = Logger.getLogger(JsonRpcDescription.class);
	private JsonRpcService	_service = null;
	private Map<String, Class>	_types = new HashMap<String, Class>();
	private Map<String, Class>	_typesProcessed = new HashMap<String, Class>();
	private static final String SDVERSION = "1.0";
	
	private static Set<Class> PRIMITIVE_NUMBERS = new HashSet<Class>()
	{{ 
		add(Byte.TYPE); add(Short.TYPE); add(Integer.TYPE); 
		add(Long.TYPE); add(Float.TYPE); add(Double.TYPE); 
	}};
	
	public JsonRpcDescription(JsonRpcService service) {
		_service = service;
	}
	
	public JSONObject describe() throws JSONException {
		return describe(_service.getServiceClass());
	}
	
	public List<String> getParamNames(JsonRpcOperation operation) {
		List<String> result = new ArrayList<String>();
		try {
			JSONArray params = getParams(operation);
		
			int length = params.size();
			for (int index = 0; index < length; index++) {
				result.add(params.getJSONObject(index).getString("name"));
			}
		} catch (JSONException e) {
			LOG.debug("Exception caught getting param names", e);
		}
		return result;
	}
	
	private JSONObject describe(Class clazz) throws JSONException {
		JSONObject result = new JSONObject();
		Map<String, Object> desc = new HashMap<String, Object>();
		
		Annotation[] serviceAnnotations = clazz.getAnnotations();
		if (serviceAnnotations.length >= 1 && serviceAnnotations[0] instanceof Service) {
			Service serviceAnnontation = (Service) serviceAnnotations[0];
			
			desc.put("sdversion", serviceAnnontation.sdversion());
			desc.put("name", serviceAnnontation.name());
			desc.put("id", serviceAnnontation.id());
			desc.put("version", serviceAnnontation.version());
			desc.put("summary", serviceAnnontation.summary());
			desc.put("help", serviceAnnontation.help());
			desc.put("address", serviceAnnontation.address());
		} else {
			desc.put("sdversion", SDVERSION);
			desc.put("name", clazz.getSimpleName());
			desc.put("id", "urn:" + clazz.getSimpleName() + ":1.0");
		}
		
		result = JSONObject.fromObject(desc);
		
		result.put("procs", getProcs());
		result.put("types", getTypes());
		
		return result;
	}
	
	private JSONArray getProcs() throws JSONException {
		JSONArray procs = new JSONArray();
		
		for (String name : _service.getOperations().keySet()) {
			List<JsonRpcOperation> operations = _service.getOperations().get(name);
			
			if (!operations.isEmpty()) { 
				JsonRpcOperation op = operations.get(0);
				Annotation[] procAnnotations = op.getMethod().getAnnotations();
				if (procAnnotations.length >= 1) {
					Proc procAnnontation = (Proc) procAnnotations[0];
					Map<String, Object> desc = new HashMap<String, Object>();
					
					desc.put("name", procAnnontation.name());
					desc.put("summary", procAnnontation.summary());
					desc.put("help", procAnnontation.help());
					desc.put("idempotent", procAnnontation.idempotent());
					
					desc.put("params", getParams(op));
					
					JSONObject returnType = new JSONObject();
					returnType.put("type", getType(op.getMethod().getReturnType(), op.getMethod().getGenericReturnType()));
					desc.put("return", returnType);
					
					JSONObject proc = JSONObject.fromObject(desc);
					procs.add(proc);
				} else {
					Map<String, Object> desc = new HashMap<String, Object>();
					
					desc.put("name", op.getMethod().getName());
					desc.put("idempotent", false);
					
					desc.put("params", getParams(op));
					
					JSONObject returnType = new JSONObject();
					returnType.put("type", getType(op.getMethod().getReturnType(), op.getMethod().getGenericReturnType()));
					desc.put("return", returnType);
					
					JSONObject proc = JSONObject.fromObject(desc);
					procs.add(proc);
				}
			}
		}
		
		return procs;
	}
	
	private JSONArray getParams(JsonRpcOperation operation) throws JSONException {
		return getParams(operation.getMethod());
	}
	
	private JSONArray getParams(Method method) throws JSONException {
		JSONArray params = new JSONArray();
		
		Type[] genericTypes = method.getGenericParameterTypes();
		int index = 0;
		for (Class param : method.getParameterTypes()) {
			Annotation[][] paramsAnnotations = method.getParameterAnnotations();

			for (Annotation annotation : paramsAnnotations[index]) {
				Param paramAnnotation = (Param) annotation;
				JSONObject paramObj = new JSONObject();
				paramObj.put("name", paramAnnotation.name());
				paramObj.put("type", getType(param, genericTypes[index]));
				params.add(paramObj);
			}
			// TODO: add reflection here
			index++;
		}
		
		return params;
	}
	
	/**
	 * Parameter and Return Types:
	 *	"bit"
	 *	    Boolean
	 *	"num"
	 *	    Number
	 *	"str"
	 *	    String
	 *	"arr"
	 *	    Array
	 *	"obj"
	 *	    Object
	 *	"any"
	 *	    Boolean, Number, String, Array or Object
	 *	"nil"
	 *	    None (valid only for procedure return type) 
	 */
	private String getType(Class clazz, Type genericType) {
		String result = "any";
		
		if (clazz == null) {
			result = "nil";
		} else if (Number.class.isAssignableFrom(clazz) || isPrimitiveNumber(clazz)) {
			result = getNumberType(clazz);
		} else if (String.class.isAssignableFrom(clazz) || Character.TYPE.equals(clazz)) {
			result = "str";
		} else if (Boolean.class.isAssignableFrom(clazz) || Boolean.TYPE.equals(clazz)) {
			result = "bit/Boolean";
		} else if (Collection.class.isAssignableFrom(clazz)) {
			result = "arr";
			Class arrayType = ClassUtils.getArrayElementType(genericType);
			if (arrayType != null) {
				result += "/" + arrayType.getSimpleName();
			}
		} else if (clazz.isEnum()) {
			result = getEnumConstants(clazz);
		} else if (ClassUtils.isMapType(clazz)) {
			result = "obj";
		} else {
			result = "obj/" + clazz.getSimpleName();
			_types.put(clazz.getSimpleName(), clazz);
		}
		
		return result;
	}
	
	private JSONObject getTypes() throws JSONException {
		JSONObject types = new JSONObject();
		
		while (!_types.isEmpty()) {
			Map<String, Class> safeTypes = new HashMap<String, Class>(_types);
			for (String key : safeTypes.keySet()) {
				if (!_typesProcessed.containsKey(key)) {
					Class value = safeTypes.get(key);
					types.put(key, describeType(value));
					_typesProcessed.put(key, value);
				}
			}
			_types.keySet().removeAll(_typesProcessed.keySet());
		}
		
		return types;
	}
	
	
	private boolean isPrimitiveNumber(Class clazz) {
		return clazz.isPrimitive() && PRIMITIVE_NUMBERS.contains(clazz);
	}
	
	
	private String getNumberType(Class number) {
		String result = "num/";
		if (Integer.class.isAssignableFrom(number) || Integer.TYPE.equals(number)) {
			result += "Integer";
		} else if (Double.class.isAssignableFrom(number) || Double.TYPE.equals(number)) {
			result += "Double";
		} else if (Float.class.isAssignableFrom(number) || Float.TYPE.equals(number)) {
			result += "Float";
		} else if (Short.class.isAssignableFrom(number) || Short.TYPE.equals(number)) {
			result += "Short";
		} else if (Long.class.isAssignableFrom(number) || Long.TYPE.equals(number)) {
			result += "Long";
		} else if (Byte.class.isAssignableFrom(number) || Byte.TYPE.equals(number)) {
			result += "Long";
		}
		return result;
	}
	
	private String getEnumConstants(Class enumType) {
		StringBuilder enumConsts = new StringBuilder("str/enum[");
		boolean firstTime = true;
		for (Object constant : enumType.getEnumConstants()) {
			if (!firstTime) {
				enumConsts.append(',');
			}
			enumConsts.append(constant.toString());
			firstTime = false;
		}
		enumConsts.append(']');
		return enumConsts.toString();
	}
	
	private JSONObject describeType(Class clazz) throws JSONException {
		JSONObject procs = new JSONObject();
		
		PropertyUtilsBean propertyUtils = new PropertyUtilsBean();
		
		for (PropertyDescriptor propertyDesc : propertyUtils.getPropertyDescriptors(clazz)) {
			if (!propertyDesc.getName().equals("class")) {
				Type propertyType = propertyDesc.getPropertyType();
				Type[] genericParameterTypes = null;
				if (propertyDesc.getWriteMethod() != null) {
					genericParameterTypes = propertyDesc.getWriteMethod().getGenericParameterTypes();
				} else if (propertyDesc.getReadMethod() != null) {
					genericParameterTypes = propertyDesc.getReadMethod().getGenericParameterTypes();
				}
				
				Type genericType = null;
				if (genericParameterTypes != null && genericParameterTypes.length > 0) {
					genericType = genericParameterTypes[0];
				}
				
				procs.put(propertyDesc.getName(), getType((Class)propertyType, genericType));
			}
		}
		
		return procs;
	}
	
}
